Skip to content

feat(backup): include LaunchAgent plist in EXTERNAL_PATHS for macOS DR#75

Open
mt-alarcon wants to merge 1 commit into
evolution-foundation:mainfrom
mt-alarcon:pr/backup-launchagent-plist
Open

feat(backup): include LaunchAgent plist in EXTERNAL_PATHS for macOS DR#75
mt-alarcon wants to merge 1 commit into
evolution-foundation:mainfrom
mt-alarcon:pr/backup-launchagent-plist

Conversation

@mt-alarcon
Copy link
Copy Markdown

@mt-alarcon mt-alarcon commented May 11, 2026

Problem

On macOS, EvoNexus runs as a LaunchAgent (~/Library/LaunchAgents/com.mta.evonexus.plist). Without this file, the service cannot be restored after a disaster recovery — the plist defines the run schedule, working directory, log paths, and environment.

backup.py backed up the workspace and auto-memory directory, but silently skipped the plist.

Changes

backup.py — 2 additions

1. EXTERNAL_PATHS dict (replaces the single _AUTO_MEMORY_PATH variable):

EXTERNAL_PATHS = {
    Path.home() / ".claude" / "projects" / _workspace_slug / "memory": "_external/auto-memory",
    Path.home() / "Library" / "LaunchAgents" / "com.mta.evonexus.plist": "_external/launchagents/com.mta.evonexus.plist",
}

2. backup_local loop now handles both files and directories:

for ext_root, zip_prefix in EXTERNAL_PATHS.items():
    if not ext_root.exists():
        continue
    if ext_root.is_file():
        external_files.append((ext_root, zip_prefix))
    else:
        for f in sorted(ext_root.rglob("*")):
            if f.is_file():
                rel_in_zip = zip_prefix + "/" + f.relative_to(ext_root).as_posix()
                external_files.append((f, rel_in_zip))

Compatibility

  • Existing EXTERNAL_PATHS entries (auto-memory directory) continue to work unchanged
  • File entries that don't exist on disk are silently skipped (.exists() check)
  • Non-macOS deployments: plist path won't exist → skipped automatically, zero regression
  • ZIP layout unchanged for directory entries

Risk

Low. Purely additive:

  • Adds at most 1 file (~4 KB) to each backup ZIP
  • No changes to workspace file collection logic
  • Tested locally: backup ZIP contains _external/launchagents/com.mta.evonexus.plist as expected

Summary by Sourcery

Include additional external macOS-specific files in local backups to support disaster recovery of the EvoNexus service.

New Features:

  • Add backing up of the EvoNexus LaunchAgent plist from the user's LaunchAgents directory into the backup archive.
  • Support configuring multiple external file and directory paths to be included in backups via a shared mapping.

Enhancements:

  • Generalize external path handling in local backups to support both single files and directories discovered via recursive traversal.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 11, 2026

Reviewer's Guide

Adds a configurable EXTERNAL_PATHS mapping and updates backup_local to include both directory-based auto-memory and a macOS LaunchAgent plist in backups, handling both file and directory external paths safely.

File-Level Changes

Change Details Files
Introduce EXTERNAL_PATHS mapping for external backup sources derived from workspace and home paths.
  • Derive a _workspace_slug from WORKSPACE by replacing path separators with dashes to match Claude project slug format.
  • Replace the single auto-memory path variable with an EXTERNAL_PATHS dict mapping Paths to ZIP prefixes.
  • Add a macOS-specific LaunchAgent plist path to EXTERNAL_PATHS, targeting an _external/launchagents path inside the ZIP.
backup.py
Extend backup_local to collect external files from EXTERNAL_PATHS, supporting both files and directories.
  • Initialize an external_files list of (Path, zip-path) tuples before ZIP creation.
  • Iterate EXTERNAL_PATHS, skipping entries that do not exist on disk.
  • Handle both file and directory entries: add a single file directly with its ZIP path or recursively glob directory contents, preserving relative structure under the configured ZIP prefix.
backup.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The _workspace_slug derivation using str(WORKSPACE).replace('/', '-') assumes POSIX-style separators and may produce unexpected slugs on Windows or non-standard paths; consider using WORKSPACE.as_posix() and/or a more explicit slugging function to ensure consistent, cross-platform-safe values.
  • The LaunchAgent path is hardcoded to com.mta.evonexus.plist; if this identifier ever changes or multiple variants are supported, it might be safer to either derive this from configuration or allow the path to be overridden rather than baking it into EXTERNAL_PATHS.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_workspace_slug` derivation using `str(WORKSPACE).replace('/', '-')` assumes POSIX-style separators and may produce unexpected slugs on Windows or non-standard paths; consider using `WORKSPACE.as_posix()` and/or a more explicit slugging function to ensure consistent, cross-platform-safe values.
- The LaunchAgent path is hardcoded to `com.mta.evonexus.plist`; if this identifier ever changes or multiple variants are supported, it might be safer to either derive this from configuration or allow the path to be overridden rather than baking it into `EXTERNAL_PATHS`.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Adiciona com.mta.evonexus.plist ao backup S3. Patch o loop de
EXTERNAL_PATHS para suportar entradas do tipo arquivo além de
diretório (Opção A): is_file() → adiciona direto; is_dir() → rglob.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mt-alarcon mt-alarcon force-pushed the pr/backup-launchagent-plist branch from 8f70491 to 1899467 Compare May 12, 2026 10:54
@mt-alarcon
Copy link
Copy Markdown
Author

Addressed both review points from @sourcery-ai in 1899467 (force-pushed to pr/backup-launchagent-plist):

  1. _workspace_slug cross-platform — switched from str(WORKSPACE).replace("/", "-") to WORKSPACE.as_posix().replace("/", "-") so the slug is consistent regardless of platform.

  2. com.mta.evonexus.plist no longer hardcoded — moved to an opt-in env var:

    _launchagent_plist = os.environ.get("BACKUP_LAUNCHAGENT_PLIST", "").strip()
    if _launchagent_plist:
        _la_path = Path(_launchagent_plist).expanduser()
        EXTERNAL_PATHS[_la_path] = f"_external/launchagents/{_la_path.name}"

    Users who want their macOS LaunchAgent in the backup set BACKUP_LAUNCHAGENT_PLIST=/path/to/file.plist in .env. The ZIP entry name is derived from Path.name, so no deployment-specific identifier is baked into the code.

Tested locally — EXTERNAL_PATHS now contains the plist when the env var is set, and falls back to just the auto-memory path when unset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant